#pragma once

#include "Utilities.h"

#include <assert.h>

#include <stdexcept>
#include <vector>
#include <unordered_map>

/**
 *	This Table will hold elements of type T,
 *	and will have the columns headed by values
 *	of type H.
 */
template < typename H, typename T > 
class Table {

public:
	
	typedef std::vector<T> Column;
	typedef std::unordered_map<H, Column> ColumnMap;
	typedef std::unordered_map<T, int> ColumnItemCountMap;
	typedef std::unordered_map<H, ColumnItemCountMap> ColumnItemCountMaps;
	
	static const int ERROR_GETCOLUMNSIZE_NOSUCHHEADER;
	static const int ERROR_DUPLICATION_BADROWINDEX;
	static const int ERROR_DUPLICATION_BADNUMOFTIMES;

	/**
	 *	Default constructor that 
	 *	constructs an empty table, with no elements/headers.
	 */
	Table<H,T>(void);
	/**
	 *	2-empty-column constructor that
	 *	constructs a table and inserts
	 *	2 empty columns with headers header1 and header2.
	 */
	Table<H,T>(const H& header1, const H& header2);
	~Table<H,T>(void);

	/**
	 *	Inserts a column with the given header,
	 *	and returns true if successfully inserted.
	 *	Returns false and does not insert a column
	 *	if the column with the specified header
	 *	is already in the table.
	 */
	bool insertColumn(const H& header);

	/**
	 *	Inserts an entry at the column of the specified header
	 *	and at the specified rowIndex with the given item.
	 *	Does not insert anything and returns false
	 *	if the header does not exist, 
	 *	or the rowIndex is greater than the size of the specified column.
	 *	Otherwise, inserts the item at the specified column at the specified
	 *	rowIndex, increasing the size of the column by 1 and 
	 *	returns true.
	 */
	bool appendEntry(const H& header, const T& item);

	/**
	 *	Modifies an entry at the column of the specified header
	 *	and at the speicifed rowIndex with the given item.
	 *	Does not insert anything and returns false if the header does not exit,
	 *	or the rowIndex is greater than the size of the specified column.
	 *	Otherwise, inserts the item at the specified column at the specified rowIndex, 
	 *	increasing the size of the column by 1 and returns true.
	 */
	bool modifyEntry(const H& header, int rowIndex, const T& item);

	/**
	 *	Returns the entry at the column of the specified header at the
	 *	specified rowIndex.
	 *	However, if no such entry currently exists (no such header or
	 *	no such rowIndex for that column at that header) then 
	 *	an exception is thrown, to indicate that there is no such entry.
	 */
	const T& getEntry(const H& header, int rowIndex) const;

	/**
	 *	Returns the column size of the column of the specified header.
	 *	If there is no such column in the table, returns -1.
	 */
	int getColumnSize(const H& header) const;

	/**
	 *	Returns the number of rows in this table 
	 *	(the maximum size of a column).
	 */
	int getNumRows() const;

	/**
	 *	Returns a vector containing the existing headers.
	 *	May be empty.
	 */
	std::vector<H>& getHeaders() const;

	/**
	 *	Returns a vector containing the existing columns.
	 *	May be empty.
	 */
	std::vector<Column>& getColumns() const;

	/**
	 *	Returns a copy of the column of the specified header.
	 *	May be empty, even when the specified header does not exist.
	 */
	Column& getColumn(const H& header) const;

	/**
	 *	Returns a copy of the column's item count unordered_map.
	 *	May be empty, even when the specified header does not exist.
	 */
	ColumnItemCountMap& getColumnItemCountMap(const H& header) const;

	/**
	 *	Duplicates the #rowIndex row #numOfTimes.
	 *	Returns the index of the last duplicated row after duplicating.
	 *	If the rowIndex does not exist (i.e. rowIndex >= mNumRows 
	 *	or rowIndex < 0) then does nothing and returns -1 to indicate it.
	 *	Also does nothing and returns -2 if numOfTimes < 1.
	 */
	int duplicateRow(int rowIndex, int numOfTimes);

	/**
	 *	Deletes the #rowIndex row, and returns true if successful.
	 *	Returns false when the rowIndex < 0 or rowIndex >= number of rows in the table.
	 */
	bool deleteRow(int rowIndex);

	/**
	 *	Checks if the header exists.
	 *	Returns true if so and false otherwise.
	 */
	bool doesHeaderExist(const H& header) const;

private:

	ColumnMap mColumnMap; // Map of the columns, keyed by the header
	ColumnItemCountMaps mColumnItemCountMaps; // Map of the column item count unordered_maps, keyed by the header
	unsigned int mNumRows;

	/**
	 *	To be used only immediately after an insertion into a column.
	 *	Updates the mNumRows value to the updatedColumnSize if the mNumRows
	 *	value is smaller.
	 */
	void updateNumRowsAfterInsertionIfNecessary(unsigned int updatedColumnSize) {
		if (mNumRows < updatedColumnSize) {
			mNumRows = updatedColumnSize;
		}
	}

	/**
	 *	To be used only immediately after an insertion into a column.
	 *	Updates the ColumnItemCountMap for the respective column.
	 *	Might throw an exception to indicate that header was not found in the 
	 *	mColumnItemCountMaps to indicate programmer error.
	 */
	void updateColumnItemCountMapAfterInsertion(const H& header, const T& item) {
		if (mColumnItemCountMaps.find(header) == mColumnItemCountMaps.end()) {
			throw std::out_of_range("No such header in mColumnItemCountMaps");
		}
		ColumnItemCountMap& columnItemCountMap = mColumnItemCountMaps.at(header);
		// Ensure that the columnItemCountMap contains a key for the item
		if ( !Utilities::isKeyInMap_TypeType(columnItemCountMap, item)) {
			columnItemCountMap.insert( std::pair<T, int>(item, 0) );
		}
		// Here, we are ensured that the columnItemCountMap contains a key for the item
		// so just increase the count
		columnItemCountMap[item]++;
	}

	/**
	 *	To be used only immediately after a deletion from a column.
	 *	Updates the ColumnItemCountMap for the respective column.
	 *	Might throw an exception to indicate that header was not found in the 
	 *	mColumnItemCountMaps, or somehow itemDeleted was not found in the
	 *	column's ColumnItemCountMap (it has to be there in the first place to be deleted)
	 *	to indicate programmer error.
	 */
	void updateColumnItemCountMapAfterDeletion(const H& header, const T& itemDeleted) {
		if (mColumnItemCountMaps.find(header) == mColumnItemCountMaps.end()) {
			throw std::out_of_range("No such header in mColumnItemCountMaps");
		}
		ColumnItemCountMap& columnItemCountMap = mColumnItemCountMaps.at(header);
		// Ensure that the columnItemCountMap contains a key for the item
		if ( !Utilities::isKeyInMap_TypeType(columnItemCountMap, itemDeleted)) {
			throw std::out_of_range("No such itemDeleted in columnItemCountMap");
		}
		// Here, we are ensured that the columnItemCountMap contains a key for the item
		// so just decrease the count, and remove the entry from the
		// count unordered_map if the count reaches 0
		columnItemCountMap[itemDeleted]--;
		if (columnItemCountMap[itemDeleted] == 0) {
			columnItemCountMap.erase(itemDeleted);
		}
	}
};

template < typename H, typename T >
const int Table<H,T>::ERROR_GETCOLUMNSIZE_NOSUCHHEADER = -1;
template < typename H, typename T >
const int Table<H,T>::ERROR_DUPLICATION_BADROWINDEX = -1;
template < typename H, typename T >
const int Table<H,T>::ERROR_DUPLICATION_BADNUMOFTIMES = -2;

template < typename H, typename T >
Table<H,T>::Table(void) {
	mNumRows = 0;
}

template < typename H, typename T >
Table<H,T>::Table(const H& header1, const H& header2) {
	mNumRows = 0;

	bool isInsertSuccessful1 = insertColumn(header1);
	bool isInsertSuccessful2 = insertColumn(header2);

	assert(isInsertSuccessful1 && isInsertSuccessful2);
}

template < typename H, typename T >
Table<H,T>::~Table(void) {}

template < typename H, typename T >
bool Table<H,T>::insertColumn(const H& header) {
	if (mColumnMap.find(header) != mColumnMap.end()) {
		// Already has this column inserted
		return false;
	}

	// Insert a column,
	mColumnMap.insert( std::pair<H, Column>(header, Column()) );
	// and insert a column item count unordered_map
	mColumnItemCountMaps.insert( std::pair<H, ColumnItemCountMap>(header, ColumnItemCountMap()) );
	return true;
}

template < typename H, typename T >
bool Table<H,T>::appendEntry(const H& header, const T& item) {
	// Ensure that header has a column for it
	if (mColumnMap.find(header) == mColumnMap.end()) {
		return false;
	}
	// Here, we are ensured that the header has a column for it.
	Column& column = mColumnMap.at(header);
	column.push_back(item);

	// Update the ColumnItemCountMap
	updateColumnItemCountMapAfterInsertion(header, item);

	// Update the number of rows in the table after insertion
	unsigned int updatedColumnSize = column.size();
	updateNumRowsAfterInsertionIfNecessary(updatedColumnSize);

	return true;
}

template < typename H, typename T >
bool Table<H,T>::modifyEntry(const H& header, int rowIndex, const T& item) {
	// Ensure that header has a column for it
	if (mColumnMap.find(header) == mColumnMap.end()) {
		return false;
	}
	
	// Here, we are ensured that the header has a column for it.
	Column& column = mColumnMap.at(header);
	// Ensure that the rowIndex is valid
	if (rowIndex < 0 || rowIndex >= (int)column.size()) {
		return false;
	}

	// Here, we are ensured that the requested element can be modified. 
	// Modify it!
	int prevItem = column.at(rowIndex);
	column.at(rowIndex) = item;

	updateColumnItemCountMapAfterDeletion(header, prevItem);
	updateColumnItemCountMapAfterInsertion(header, item);

	return true;
}

template < typename H, typename T >
const T& Table<H,T>::getEntry(const H& header, int rowIndex) const {
	// Ensure that there is a column for such a header in the table
	if (mColumnMap.find(header) == mColumnMap.end()) {
		throw std::out_of_range("No such header in Table");
	}
	
	const Column& column = mColumnMap.at(header);
	// Ensure that there is such a rowIndex for the column
	// i.e. 0 <= rowIndex < column.size()
	if (rowIndex < 0 || rowIndex >= (int)column.size()) {
		throw std::out_of_range("rowIndex does not exist for the specified column");
	}

	// Here, we know that the entry is valid
	return column.at(rowIndex);
}

template < typename H, typename T >
int Table<H,T>::getColumnSize(const H& header) const {
	// Ensure that there is such a column for the specified header
	if (mColumnMap.find(header) == mColumnMap.end()) {
		return ERROR_GETCOLUMNSIZE_NOSUCHHEADER;
	}

	// Here, we are ensured that there is such a column, so return its size
	return mColumnMap.at(header).size();
}

template < typename H, typename T >
int Table<H,T>::getNumRows() const {
	return mNumRows;
}

template < typename H, typename T >
std::vector<H>& Table<H,T>::getHeaders() const {
	std::vector<H>* headers = new std::vector<H>();
	for (auto it = mColumnMap.begin(); it != mColumnMap.end(); it++) {
		headers->push_back(it->first);
	}
	return *headers;
}

template < typename H, typename T >
std::vector<std::vector<T>>& Table<H,T>::getColumns() const {
	std::vector<Column>* columns = new std::vector<Column>();
	for (auto it = mColumnMap.begin(); it != mColumnMap.end(); it++) {
		columns->push_back(it->second);
	}
	return *columns;
}

template < typename H, typename T >
std::vector<T>& Table<H,T>::getColumn(const H& header) const {
	Column *column = new std::vector<T>();
	// Ensure the header exists
	if (mColumnMap.find(header) == mColumnMap.end()) {
		return *column;
	}

	// Here, we are ensured that there is a column for the specified header.
	*column = mColumnMap.at(header);
	return *column;
}

template < typename H, typename T >
std::unordered_map<T, int>& Table<H,T>::getColumnItemCountMap(const H& header) const {
	ColumnItemCountMap *columnItemCountMap = new ColumnItemCountMap();
	// Ensure the header exists
	if (mColumnItemCountMaps.find(header) == mColumnItemCountMaps.end()) {
		return *columnItemCountMap;
	}

	// Here, we are ensured that there is a column item count unordered_map for the specified header.
	*columnItemCountMap = mColumnItemCountMaps.at(header);
	return *columnItemCountMap;
}

template < typename H, typename T >
int Table<H,T>::duplicateRow(int rowIndex, int numOfTimes) {
	// Ensure that there is such a rowIndex
	if (rowIndex < 0 || rowIndex >= (int)mNumRows) {
		return ERROR_DUPLICATION_BADROWINDEX;
	}
	// Ensure that the specified numOfTimes is at least 1
	if (numOfTimes < 1) {
		return ERROR_DUPLICATION_BADNUMOFTIMES; 
	}

	// Here, we are ensured that the row of #rowIndex exists, and
	// that numOfTimes is at least 1.
	
	// Duplicate each column #numOfTimes
	for (auto it = mColumnMap.begin(); it != mColumnMap.end(); it++) {
		const H& header = it->first;
		const Column& column = it->second;

		// Ensure that the column has an entry at #rowIndex,
		// otherwise don't copy anything for the column
		if (rowIndex >= (int)column.size()) {
			continue;
		} else {

			// Here, we are ensured that the column has an entry at #rowIndex
			const T item = column.at(rowIndex);
			for (int i = 1; i <= numOfTimes; i++) {
				appendEntry(header, item);
			}
		}
	}
	return getNumRows();
}

template < typename H, typename T >
bool Table<H,T>::deleteRow(int rowIndex) {
	// Ensure that 0 < rowIndex < mNumRows
	if (rowIndex < 0 || rowIndex >= (int)mNumRows) {
		return false;
	}

	// Here, we are ensured that there is a #rowIndex row in the table.

	// Delete the row
	for (auto it = mColumnMap.begin(); it != mColumnMap.end(); it++) {
		// Erase the entry at #rowIndex in each column
		Column& column = it->second;

		// Ensure that the #rowIndex exists
		if (rowIndex >= (int)column.size()) {
			continue;
		}

		// Here, we are ensured that #rowIndex exists for the current column
		// Don't use reference for itemDelete because it will be erased
		const T itemDeleted = column.at(rowIndex); // For updating the ColumnItemCountMap
		const H& header = it->first; // For updating the ColumnItemCountMap

		// Erase the entry
		column.erase(column.begin() + rowIndex);

		// Update the ColumnItemCountMap
		updateColumnItemCountMapAfterDeletion(header, itemDeleted);
	}

	// Decrement the numRows since we just removed one row in the table
	mNumRows--;

	return true;
}

template < typename H, typename T >
bool Table<H,T>::doesHeaderExist(const H& header) const {
	return Utilities::isKeyInMap_TypeType(mColumnMap, header);
}